Skip to main content
Phoenix LiveView provides different approaches for testing function components and LiveComponents, from simple unit tests to full integration tests.

Testing Function Components

Function components are pure functions that return HEEx templates. There are two main approaches to testing them.

Using render_component/3

The render_component/3 function renders a component and returns its HTML:
defmodule MyAppWeb.ComponentsTest do
  use ExUnit.Case, async: true
  
  import Phoenix.LiveViewTest
  
  test "greets user" do
    assert render_component(&MyComponents.greet/1, name: "Mary") ==
             "<div>Hello, Mary!</div>"
  end
end

Using rendered_to_string/1

For complex components, use the ~H sigil with rendered_to_string/1:
defmodule MyAppWeb.ComponentsTest do
  use ExUnit.Case, async: true
  
  import Phoenix.Component
  import Phoenix.LiveViewTest
  
  test "greets user with HEEx" do
    assigns = %{}
    
    assert rendered_to_string(~H"""
           <MyComponents.greet name="Mary" />
           """) == "<div>Hello, Mary!</div>"
  end
end
Use rendered_to_string/1 when you need to compose multiple components or test components with slots and complex structures.

Testing Function Components with Props

Test components with various prop types:
test "renders user card" do
  user = %{name: "Alice", email: "alice@example.com", avatar: "/images/alice.jpg"}
  
  html = render_component(&MyComponents.user_card/1, user: user, size: :large)
  
  assert html =~ "Alice"
  assert html =~ "alice@example.com"
  assert html =~ "src=\"/images/alice.jpg\""
  assert html =~ "class=\"user-card-large\""
end

Testing Components with Slots

Test components that accept slots:
test "renders card with header and footer slots" do
  assigns = %{}
  
  html = rendered_to_string(~H"""
  <MyComponents.card>
    <:header>
      <h2>Title</h2>
    </:header>
    
    <p>Card content</p>
    
    <:footer>
      <button>Action</button>
    </:footer>
  </MyComponents.card>
  """)
  
  assert html =~ "<h2>Title</h2>"
  assert html =~ "<p>Card content</p>"
  assert html =~ "<button>Action</button>"
end

Testing Dynamic Attributes

Test components that use @rest or dynamic attributes:
test "forwards attributes to element" do
  html = render_component(
    &MyComponents.button/1,
    class: "btn-primary",
    disabled: true,
    data_test_id: "submit-button"
  )
  
  assert html =~ ~s(class="btn-primary")
  assert html =~ ~s(disabled)
  assert html =~ ~s(data-test-id="submit-button")
end

Testing LiveComponents

LiveComponents are stateful components that can handle events. You can test them in isolation or within a parent LiveView.

Testing LiveComponent Rendering

Use render_component/3 to test a LiveComponent’s initial render:
test "renders counter component" do
  assert render_component(MyAppWeb.CounterComponent, id: 1, initial_count: 5) =~
           "Count: 5"
end
The :id option is required when testing LiveComponents, as they must be uniquely identified.

Testing LiveComponent with Router

If your component uses the router, pass it as an option:
@endpoint MyAppWeb.Endpoint

test "renders component with router" do
  assert render_component(
    MyAppWeb.NavComponent,
    %{id: 1, current_path: "/users"},
    router: MyAppWeb.Router
  ) =~ "active"
end

Testing LiveComponent Events

To test events, mount the LiveComponent within a LiveView:
test "handles increment event", %{conn: conn} do
  {:ok, view, html} = live(conn, "/counter")
  
  assert html =~ "Count: 0"
  
  # Target component by element with ID
  assert view
         |> element("#counter-1 button", "Increment")
         |> render_click() =~ "Count: 1"
end
LiveView automatically targets the component based on phx-target:
<!-- In your template -->
<.live_component module={CounterComponent} id="counter-1" />

<!-- CounterComponent renders -->
<div id="counter-1" phx-target={@myself}>
  <p>Count: {@count}</p>
  <button phx-click="increment">Increment</button>
</div>

Testing Multiple LiveComponents

Test interactions with multiple instances:
test "manages multiple counter components", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/counters")
  
  # Increment first counter
  assert view
         |> element("#counter-1 button", "+")
         |> render_click() =~ "Counter 1: 1"
  
  # Increment second counter twice
  view
  |> element("#counter-2 button", "+")
  |> render_click()
  
  assert view
         |> element("#counter-2 button", "+")
         |> render_click() =~ "Counter 2: 2"
  
  # First counter unchanged
  assert render(view) =~ "Counter 1: 1"
end

Testing Component Lifecycle

Test LiveComponent lifecycle callbacks:

Testing mount/1

test "initializes state in mount" do
  html = render_component(
    MyAppWeb.TimerComponent,
    id: 1,
    duration: 60
  )
  
  assert html =~ "Time remaining: 60s"
end

Testing update/2

Test how components respond to prop changes:
test "updates when props change", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/dashboard")
  
  # Component shows initial user
  assert render(view) =~ "User: Alice"
  
  # Parent updates the user prop
  send(view.pid, {:update_user, "Bob"})
  
  # Component re-renders with new user
  assert render(view) =~ "User: Bob"
end

Testing handle_event/3

Test event handlers in components:
test "deletes user on click", %{conn: conn} do
  {:ok, view, html} = live(conn, "/users")
  
  assert html =~ "user-13"
  
  # Click delete in user component
  html = view
         |> element("#user-13 a", "Delete")
         |> render_click()
  
  refute html =~ "user-13"
  refute has_element?(view, "#user-13")
end

Testing Component Composition

Test components that render other components:
test "renders nested components" do
  assigns = %{users: [
    %{id: 1, name: "Alice"},
    %{id: 2, name: "Bob"}
  ]}
  
  html = rendered_to_string(~H"""
  <MyComponents.user_list users={@users}>
    <:empty>No users found</:empty>
  </MyComponents.user_list>
  """)
  
  assert html =~ "Alice"
  assert html =~ "Bob"
  refute html =~ "No users found"
end

test "renders empty state" do
  assigns = %{users: []}
  
  html = rendered_to_string(~H"""
  <MyComponents.user_list users={@users}>
    <:empty>No users found</:empty>
  </MyComponents.user_list>
  """)
  
  assert html =~ "No users found"
end

Testing Component Slots

Named Slots

Test components with multiple named slots:
test "renders modal with slots" do
  assigns = %{}
  
  html = rendered_to_string(~H"""
  <MyComponents.modal id="confirm-modal">
    <:title>Confirm Action</:title>
    
    <:body>
      <p>Are you sure?</p>
    </:body>
    
    <:footer>
      <button>Cancel</button>
      <button>Confirm</button>
    </:footer>
  </MyComponents.modal>
  """)
  
  assert html =~ "Confirm Action"
  assert html =~ "Are you sure?"
  assert html =~ "Cancel"
  assert html =~ "Confirm"
end

Default Slot

Test the default inner block:
test "renders default slot" do
  assigns = %{}
  
  html = rendered_to_string(~H"""
  <MyComponents.container>
    <h1>Content</h1>
    <p>Inner content</p>
  </MyComponents.container>
  """)
  
  assert html =~ "<h1>Content</h1>"
  assert html =~ "<p>Inner content</p>"
end

Slot Attributes

Test slots that expose attributes:
test "renders list with item slot" do
  assigns = %{
    items: [
      %{id: 1, name: "Apple", price: 1.99},
      %{id: 2, name: "Banana", price: 0.99}
    ]
  }
  
  html = rendered_to_string(~H"""
  <MyComponents.list items={@items}>
    <:item :let={item}>
      <span>{item.name}: ${item.price}</span>
    </:item>
  </MyComponents.list>
  """)
  
  assert html =~ "Apple: $1.99"
  assert html =~ "Banana: $0.99"
end

Testing Component Forms

Test form components:
test "renders form component" do
  changeset = User.changeset(%User{}, %{})
  
  html = render_component(
    &MyComponents.user_form/1,
    form: to_form(changeset),
    action: "/users"
  )
  
  assert html =~ ~s(action="/users")
  assert html =~ ~s(name="user[name]")
  assert html =~ ~s(name="user[email]")
end

test "shows validation errors" do
  changeset = 
    %User{}
    |> User.changeset(%{name: ""})
    |> Map.put(:action, :validate)
  
  html = render_component(
    &MyComponents.user_form/1,
    form: to_form(changeset),
    action: "/users"
  )
  
  assert html =~ "can&#39;t be blank"
end

Testing CoreComponents

Test Phoenix’s built-in core components:
import MyAppWeb.CoreComponents

test "renders button" do
  assigns = %{}
  
  html = rendered_to_string(~H"""
  <.button type="submit" class="primary">
    Save Changes
  </.button>
  """)
  
  assert html =~ ~s(type="submit")
  assert html =~ ~s(class="primary")
  assert html =~ "Save Changes"
end

test "renders input with errors" do
  assigns = %{}
  
  html = rendered_to_string(~H"""
  <.input
    name="user[email]"
    type="email"
    value=""
    errors={["can't be blank"]}
  />
  """)
  
  assert html =~ ~s(name="user[email]")
  assert html =~ "can&#39;t be blank"
end
Test components with navigation:
test "renders navigation links" do
  assigns = %{current_path: "/users"}
  
  html = rendered_to_string(~H"""
  <MyComponents.nav current_path={@current_path}>
    <:link path="/users">Users</:link>
    <:link path="/posts">Posts</:link>
  </MyComponents.nav>
  """)
  
  # Current link is active
  assert html =~ ~r(<a[^>]*class="[^"]*active[^"]*"[^>]*>Users</a>)
  
  # Other link is not active
  refute html =~ ~r(<a[^>]*href="/posts"[^>]*class="[^"]*active)
end

Testing Conditional Rendering

Test components with conditional logic:
test "shows loading state" do
  html = render_component(&MyComponents.user_profile/1, user: nil, loading: true)
  
  assert html =~ "Loading..."
  refute html =~ "Email:"
end

test "shows user data when loaded" do
  user = %{name: "Alice", email: "alice@example.com"}
  html = render_component(&MyComponents.user_profile/1, user: user, loading: false)
  
  refute html =~ "Loading..."
  assert html =~ "Alice"
  assert html =~ "alice@example.com"
end

Testing Component Accessibility

Test ARIA attributes and accessibility features:
test "includes proper ARIA attributes" do
  html = render_component(
    &MyComponents.alert/1,
    type: :error,
    message: "Something went wrong"
  )
  
  assert html =~ ~s(role="alert")
  assert html =~ ~s(aria-live="polite")
  assert html =~ "Something went wrong"
end

Best Practices

1
Test Component Contracts
2
Test that components accept and render the props they’re designed for.
3
Test Edge Cases
4
Test empty states, nil values, and boundary conditions.
5
Use rendered_to_string for Integration
6
When testing how components work together, use rendered_to_string/1.
7
Test Accessibility
8
Verify ARIA attributes, roles, and semantic HTML.
9
Keep Tests Focused
10
Test one aspect per test case for clarity and maintainability.

Common Testing Patterns

Setup Helpers

Create helpers for common test data:
defmodule MyAppWeb.ComponentsTest do
  use ExUnit.Case, async: true
  
  import Phoenix.Component
  import Phoenix.LiveViewTest
  
  defp user_fixture(attrs \\ %{}) do
    Enum.into(attrs, %{
      id: 1,
      name: "Test User",
      email: "test@example.com"
    })
  end
  
  test "renders user card" do
    user = user_fixture(name: "Alice")
    html = render_component(&MyComponents.user_card/1, user: user)
    
    assert html =~ "Alice"
  end
end

Assert Macros

Create custom assertions for common checks:
defp assert_has_css_class(html, class) do
  assert html =~ ~r/class="[^"]*#{class}[^"]*"/
end

test "applies error class" do
  html = render_component(&MyComponents.input/1, errors: ["invalid"])
  assert_has_css_class(html, "input-error")
end